package de.hauschild.gmltracer.gml.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.text.StrBuilder;

import de.hauschild.gmltracer.gml.Parser;
import de.hauschild.gmltracer.gml.Token;
import de.hauschild.gmltracer.gml.impl.array.ArrayGetToken;
import de.hauschild.gmltracer.gml.impl.array.ArrayLengthToken;
import de.hauschild.gmltracer.gml.impl.array.ArrayToken;
import de.hauschild.gmltracer.gml.impl.binding.BinderToken;
import de.hauschild.gmltracer.gml.impl.binding.IdentifierToken;
import de.hauschild.gmltracer.gml.impl.control.ApplyToken;
import de.hauschild.gmltracer.gml.impl.control.FunctionToken;
import de.hauschild.gmltracer.gml.impl.control.IfToken;
import de.hauschild.gmltracer.gml.impl.value.StringToken;
import de.hauschild.gmltracer.gml.impl.value.bool.EqualsIntegerComparatorToken;
import de.hauschild.gmltracer.gml.impl.value.bool.EqualsRealComparatorToken;
import de.hauschild.gmltracer.gml.impl.value.bool.FalseToken;
import de.hauschild.gmltracer.gml.impl.value.bool.LessIntegerComparatorToken;
import de.hauschild.gmltracer.gml.impl.value.bool.LessRealComparatorToken;
import de.hauschild.gmltracer.gml.impl.value.bool.TrueToken;
import de.hauschild.gmltracer.gml.impl.value.geometry.RenderOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.geometry.RotateXToken;
import de.hauschild.gmltracer.gml.impl.value.geometry.RotateYToken;
import de.hauschild.gmltracer.gml.impl.value.geometry.RotateZToken;
import de.hauschild.gmltracer.gml.impl.value.geometry.ScaleToken;
import de.hauschild.gmltracer.gml.impl.value.geometry.TranslateToken;
import de.hauschild.gmltracer.gml.impl.value.geometry.UniformScaleToken;
import de.hauschild.gmltracer.gml.impl.value.geometry.tier1.DirectionalLightOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.geometry.tier1.PlaneOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.geometry.tier1.SphereOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.geometry.tier1.UnionOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.integer.AddIntegerOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.integer.DivideIntegerOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.integer.IntegerToken;
import de.hauschild.gmltracer.gml.impl.value.integer.ModuloIntegerOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.integer.MultiplyIntegerOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.integer.NegateIntegerOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.integer.SubtractIntegerOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.point.GetXOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.point.GetYOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.point.GetZOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.point.PointOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.real.ACosRealOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.real.ASinRealOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.real.AddRealOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.real.ClampfRealOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.real.CosRealOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.real.DivideRealOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.real.FloorRealOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.real.FracRealOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.real.MultiplyRealOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.real.NegateRealOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.real.RealToken;
import de.hauschild.gmltracer.gml.impl.value.real.SinRealOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.real.SqrtRealOperatorToken;
import de.hauschild.gmltracer.gml.impl.value.real.SubtractRealOperatorToken;

/**
 * The GML implementation of {@link Parser}.
 * 
 * @author Klaus Hauschild
 * @since 1.0.0
 */
public class GmlParser implements Parser {

  private static final String BINDING = "/";
  private static final String FUNCTION_ENDING = "}";
  private static final String FUNCTION_OPENING = "{";
  private static final String ARRAY_ENDING = "]";
  private static final String ARRAY_OPENING = "[";
  private static final String STRING_OPENING = "\"";
  private static final Map<String, Class<? extends Token>> TOKEN_CLASSES;

  static {
    TOKEN_CLASSES = new HashMap<String, Class<? extends Token>>();

    TOKEN_CLASSES.put("get", ArrayGetToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("length", ArrayLengthToken.class); //$NON-NLS-1$

    TOKEN_CLASSES.put("true", TrueToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("false", FalseToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("eqi", EqualsIntegerComparatorToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("eqf", EqualsRealComparatorToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("lessi", LessIntegerComparatorToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("lessf", LessRealComparatorToken.class); //$NON-NLS-1$

    TOKEN_CLASSES.put("if", IfToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("apply", ApplyToken.class); //$NON-NLS-1$

    TOKEN_CLASSES.put("translate", TranslateToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("scale", ScaleToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("uscale", UniformScaleToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("rotatex", RotateXToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("rotatey", RotateYToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("rotatez", RotateZToken.class); //$NON-NLS-1$

    TOKEN_CLASSES.put("sphere", SphereOperatorToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("plane", PlaneOperatorToken.class); //$NON-NLS-1$

    TOKEN_CLASSES.put("addi", AddIntegerOperatorToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("addf", AddRealOperatorToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("subi", SubtractIntegerOperatorToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("subf", SubtractRealOperatorToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("muli", MultiplyIntegerOperatorToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("mulf", MultiplyRealOperatorToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("divi", DivideIntegerOperatorToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("divf", DivideRealOperatorToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("negi", NegateIntegerOperatorToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("negf", NegateRealOperatorToken.class); //$NON-NLS-1$

    TOKEN_CLASSES.put("modi", ModuloIntegerOperatorToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("acos", ACosRealOperatorToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("asin", ASinRealOperatorToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("clampf", ClampfRealOperatorToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("cos", CosRealOperatorToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("floor", FloorRealOperatorToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("frac", FracRealOperatorToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("sin", SinRealOperatorToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("sqrt", SqrtRealOperatorToken.class); //$NON-NLS-1$

    TOKEN_CLASSES.put("point", PointOperatorToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("getx", GetXOperatorToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("gety", GetYOperatorToken.class); //$NON-NLS-1$
    TOKEN_CLASSES.put("getz", GetZOperatorToken.class); //$NON-NLS-1$

    TOKEN_CLASSES.put("light", DirectionalLightOperatorToken.class); //$NON-NLS-1$

    TOKEN_CLASSES.put("union", UnionOperatorToken.class); //$NON-NLS-1$

    TOKEN_CLASSES.put("render", RenderOperatorToken.class); //$NON-NLS-1$
  }

  @Override
  public List<Token> parse(final String program) {
    final List<Token> tokenSequence = new ArrayList<Token>();

    final StringTokenizer tokenizer = new StringTokenizer(filterComments(program));
    while (tokenizer.hasMoreTokens()) {
      final String tokenString = tokenizer.nextToken();
      final Token token = parseToken(tokenizer, tokenString);
      tokenSequence.add(token);
    }

    return tokenSequence;
  }

  private String filterComments(final String program) {
    return program.replaceAll("%.*", StringUtils.EMPTY); //$NON-NLS-1$
  }

  private Token instantiateTokenClass(final Class<? extends Token> tokenClass) {
    if (tokenClass != null) {
      try {
        return tokenClass.newInstance();
      } catch (final InstantiationException e) {
        e.printStackTrace();
      } catch (final IllegalAccessException e) {
        e.printStackTrace();
      }
    }
    return null;
  }

  private String parseBraces(final StringTokenizer tokenizer, final String openBrace, final String closeBrace) {
    final List<String> tokens = new ArrayList<String>();
    int braceCounter = 1;
    while (tokenizer.hasMoreTokens()) {
      final String anotherToken = tokenizer.nextToken();
      tokens.add(anotherToken);
      if (anotherToken.equals(openBrace)) {
        braceCounter++;
      } else if (anotherToken.equals(closeBrace)) {
        braceCounter--;
        if (braceCounter == 0) {
          tokens.remove(tokens.size() - 1);
          break;
        }
      }
    }
    final String tokenString = new StrBuilder().appendWithSeparators(tokens, " ").toString(); //$NON-NLS-1$
    if (braceCounter != 0) {
      throw new IllegalArgumentException(String.format("Brace unbalanced for expression: %s", tokenString)); //$NON-NLS-1$
    }
    return tokenString;
  }

  @SuppressWarnings("boxing")
  private Token parseToken(final StringTokenizer tokenizer, final String tokenString) {
    Token token = null;
    // integer
    if (token == null) {
      try {
        final int i = Integer.parseInt(tokenString);
        token = new IntegerToken(i);
      } catch (final NumberFormatException nfe) {
      }
    }
    // real
    if (token == null) {
      try {
        final double real = Double.parseDouble(tokenString);
        token = new RealToken(real);
      } catch (final NumberFormatException nfe) {
      }
    }
    // string
    if (token == null) {
      if (tokenString.startsWith(STRING_OPENING)) {
        token = new StringToken(tokenString.substring(1, tokenString.length() - 1));
      }
    }
    // array
    if (token == null) {
      if (tokenString.equals(ARRAY_OPENING)) {
        final String braceString = parseBraces(tokenizer, ARRAY_OPENING, ARRAY_ENDING);
        token = new ArrayToken(parse(braceString));
      }
    }
    // function
    if (token == null) {
      if (tokenString.equals(FUNCTION_OPENING)) {
        final String braceString = parseBraces(tokenizer, FUNCTION_OPENING, FUNCTION_ENDING);
        token = new FunctionToken(parse(braceString));
      }
    }
    // binding
    if (token == null) {
      if (tokenString.startsWith(BINDING)) {
        token = new BinderToken(tokenString.substring(1));
      }
    }
    // other
    if (token == null) {
      final Class<? extends Token> tokenClass = TOKEN_CLASSES.get(tokenString);
      final Token tokenByName = instantiateTokenClass(tokenClass);
      if (tokenByName != null) {
        token = tokenByName;
      }
    }
    if (token != null) {
      return token;
    }
    // nothing found -> identifier
    return new IdentifierToken(tokenString);
  }
}